/*
 * Copyright (c) 2022 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.examples.server;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPair;
import java.security.Security;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.apache.log4j.PropertyConfigurator;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig;
import org.eclipse.milo.opcua.sdk.server.identity.CompositeValidator;
import org.eclipse.milo.opcua.sdk.server.identity.UsernameIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.X509IdentityValidator;
import org.eclipse.milo.opcua.sdk.server.util.HostnameUtil;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaRuntimeException;
import org.eclipse.milo.opcua.stack.core.security.DefaultCertificateManager;
import org.eclipse.milo.opcua.stack.core.security.DefaultTrustListManager;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.transport.TransportProfile;
import org.eclipse.milo.opcua.stack.core.types.builtin.DateTime;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.enumerated.MessageSecurityMode;
import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo;
import org.eclipse.milo.opcua.stack.core.util.CertificateUtil;
import org.eclipse.milo.opcua.stack.core.util.NonceUtil;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedCertificateGenerator;
import org.eclipse.milo.opcua.stack.core.util.SelfSignedHttpsCertificateBuilder;
import org.eclipse.milo.opcua.stack.server.EndpointConfiguration;
import org.eclipse.milo.opcua.stack.server.security.DefaultServerCertificateValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.collect.Lists.newArrayList;
import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS;
import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME;
import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_X509;

import org.eclipse.milo.examples.server.config.ConfigConstants;

public class ExampleServer {
    private static final String path_02 = "";
    private final OpcUaServer server;
    private final ExampleNamespace exampleNamespace;
    Logger logger = LoggerFactory.getLogger(getClass());

    static {
        PropertyConfigurator.configure("conf" + File.separator + "log4j.properties");
        // Required for SecurityPolicy.Aes256_Sha256_RsaPss
        Security.addProvider(new BouncyCastleProvider());
        try {
            NonceUtil.blockUntilSecureRandomSeeded(10, TimeUnit.SECONDS);
        } catch (ExecutionException | InterruptedException | TimeoutException e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }

    public static void main(String[] args) throws Exception {
        ExampleServer server = new ExampleServer();

        server.startup().get();

        final CompletableFuture<Void> future = new CompletableFuture<>();

        Runtime.getRuntime().addShutdownHook(new Thread(() -> future.complete(null)));

        future.get();
    }

    public ExampleServer() throws Exception {
        Path securityTempDir = Paths.get(System.getProperty("user.dir"), "server", "security");
        Files.createDirectories(securityTempDir);  // 创建目录C:\Users\DUANMI~1.HOL\AppData\Local\Temp\server\security
        if (!Files.exists(securityTempDir)) {
            throw new Exception("unable to create security temp dir: " + securityTempDir);
        }

        File pkiDir = securityTempDir.resolve("pki").toFile();
        logger.info("security dir: {}", securityTempDir.toAbsolutePath());
        logger.info("security pki dir: {}", pkiDir.getAbsolutePath());
        // 生成example-server.pfx文件
        KeyStoreLoader loader = new KeyStoreLoader().load(securityTempDir);
        DefaultCertificateManager certificateManager = new DefaultCertificateManager(loader.getServerKeyPair(), loader.getServerCertificateChain());
        DefaultTrustListManager trustListManager = new DefaultTrustListManager(pkiDir);
        DefaultServerCertificateValidator certificateValidator = new DefaultServerCertificateValidator(trustListManager);
        // If you need to use multiple certificates you'll have to be smarter than this.
        X509Certificate certificate = certificateManager.getCertificates().stream().findFirst().orElseThrow(() -> new UaRuntimeException(StatusCodes.Bad_ConfigurationError, "no certificate found"));
        // The configured application URI must match the one in the certificate(s)
        String applicationUri = CertificateUtil.getSanUri(certificate).orElseThrow(() -> new UaRuntimeException(StatusCodes.Bad_ConfigurationError, "certificate is missing the application URI"));
        Set<EndpointConfiguration> endpointConfigurations = createEndpointConfigurations(certificate);

        // 用户名密码验证
        UsernameIdentityValidator identityValidator = new UsernameIdentityValidator(true,
                authChallenge -> {
                    String[] loginCount = ConfigConstants.loginCount;
                    if(loginCount.length == 2){
                        String username = authChallenge.getUsername();
                        String password = authChallenge.getPassword();
//                        boolean userOk = username_01.equals(username) && password_01.equals(password);
//                        boolean adminOk = username_02.equals(username) && password_02.equals(password);
//                        return userOk || adminOk;
                        return loginCount[0].equals(username) && loginCount[1].equals(password);
                    } else {
                        return false;
                    }
                }
        );
        // 证书验证
        X509IdentityValidator x509IdentityValidator = new X509IdentityValidator(c -> true);
//        X509IdentityValidator x509IdentityValidator = new X509IdentityValidator(new Predicate<X509Certificate>() {
//            @Override
//            public boolean test(X509Certificate x509Certificate) {
//                //logger.info(x509Certificate.toString());
//                try {
//                    logger.info(Base64.getEncoder().encodeToString(x509Certificate.getEncoded()));
//                    logger.info(Base64.getEncoder().encodeToString(x509Certificate.getPublicKey().getEncoded()));
//                    logger.info(x509Certificate);
//                } catch (CertificateEncodingException e) {
//                    e.printStackTrace();
//                }
//                return true;
//            }
//        });



        // https相关
        KeyPair httpsKeyPair = SelfSignedCertificateGenerator.generateRsaKeyPair(2048);
        SelfSignedHttpsCertificateBuilder httpsCertificateBuilder = new SelfSignedHttpsCertificateBuilder(httpsKeyPair);
        httpsCertificateBuilder.setCommonName(HostnameUtil.getHostname());
        HostnameUtil.getHostnames("0.0.0.0").forEach(httpsCertificateBuilder::addDnsName);
        X509Certificate httpsCertificate = httpsCertificateBuilder.build();



        OpcUaServerConfig serverConfig = OpcUaServerConfig.builder()
                .setProductUri("urn:eclipse:milo:example-server")
                .setApplicationUri(applicationUri)
                .setApplicationName(LocalizedText.english("Eclipse Milo OPC UA Example Server"))
                .setBuildInfo(new BuildInfo("urn:eclipse:milo:example-server", "eclipse", "eclipse milo example server", OpcUaServer.SDK_VERSION, "", DateTime.now()))


                .setCertificateManager(certificateManager)
                .setTrustListManager(trustListManager)
                .setCertificateValidator(certificateValidator)
                .setIdentityValidator(new CompositeValidator(identityValidator, x509IdentityValidator))

                .setHttpsKeyPair(httpsKeyPair)
                .setHttpsCertificateChain(new X509Certificate[]{httpsCertificate})

                .setEndpoints(endpointConfigurations)
                .build();

        server = new OpcUaServer(serverConfig);
        exampleNamespace = new ExampleNamespace(server);
        exampleNamespace.startup();
    }

    private Set<EndpointConfiguration> createEndpointConfigurations(X509Certificate certificate) {
        Set<EndpointConfiguration> endpointConfigurations = new LinkedHashSet<>();

        List<String> bindAddresses = newArrayList();
        bindAddresses.add("0.0.0.0");

        Set<String> hostnames = new LinkedHashSet<>();
        hostnames.add(HostnameUtil.getHostname());
        hostnames.addAll(HostnameUtil.getHostnames("0.0.0.0"));

        for (String bindAddress : bindAddresses) {
            for (String hostname : hostnames) {
                EndpointConfiguration.Builder builder = EndpointConfiguration.newBuilder().setBindAddress(bindAddress).setHostname(hostname).setPath(ConfigConstants.path_01).setCertificate(certificate).addTokenPolicies(USER_TOKEN_POLICY_ANONYMOUS, USER_TOKEN_POLICY_USERNAME, USER_TOKEN_POLICY_X509);

                endpointConfigurations.add(buildTcpEndpoint(builder.copy().setSecurityPolicy(SecurityPolicy.None).setSecurityMode(MessageSecurityMode.None)));
                endpointConfigurations.add(buildHttpsEndpoint(builder.copy().setSecurityPolicy(SecurityPolicy.None).setSecurityMode(MessageSecurityMode.None)));

                // TCP Basic256Sha256 / SignAndEncrypt      HTTPS Basic256Sha256 / Sign (SignAndEncrypt not allowed for HTTPS)
                endpointConfigurations.add(buildTcpEndpoint(builder.copy().setSecurityPolicy(SecurityPolicy.Basic256Sha256).setSecurityMode(MessageSecurityMode.SignAndEncrypt)));
                endpointConfigurations.add(buildHttpsEndpoint(builder.copy().setSecurityPolicy(SecurityPolicy.Basic256Sha256).setSecurityMode(MessageSecurityMode.Sign)));
                /*
                 * It's good practice to provide a discovery-specific endpoint with no security.
                 * It's required practice if all regular endpoints have security configured.
                 *
                 * Usage of the  "/discovery" suffix is defined by OPC UA Part 6:
                 *
                 * Each OPC UA Server Application implements the Discovery Service Set. If the OPC UA Server requires a
                 * different address for this Endpoint it shall create the address by appending the path "/discovery" to
                 * its base address.
                 */
                EndpointConfiguration.Builder discoveryBuilder = builder.copy().setPath(path_02).setSecurityPolicy(SecurityPolicy.None).setSecurityMode(MessageSecurityMode.None);
                endpointConfigurations.add(buildTcpEndpoint(discoveryBuilder));
                endpointConfigurations.add(buildHttpsEndpoint(discoveryBuilder));
            }
        }
        return endpointConfigurations;
    }

    private static EndpointConfiguration buildTcpEndpoint(EndpointConfiguration.Builder base) {
        return base.copy().setTransportProfile(TransportProfile.TCP_UASC_UABINARY).setBindPort(ConfigConstants.tcpBindPort).build();
    }

    private static EndpointConfiguration buildHttpsEndpoint(EndpointConfiguration.Builder base) {
        return base.copy().setTransportProfile(TransportProfile.HTTPS_UABINARY).setBindPort(ConfigConstants.httpsBindPort).build();
    }

    public OpcUaServer getServer() {
        return server;
    }

    public CompletableFuture<OpcUaServer> startup() {
        return server.startup();
    }

    public CompletableFuture<OpcUaServer> shutdown() {
        exampleNamespace.shutdown();

        return server.shutdown();
    }
}
